UNPKG

8.75 kBJavaScriptView Raw
1// Hacking too much time
2
3// Based on Node.js Module class sources:
4// https://github.com/nodejs/node/blob/master/lib/module.js
5
6import fs from 'fs'
7import path from 'path'
8import Module from 'module'
9
10import Log from './tools/log'
11import { exists, ends_with } from './helpers'
12
13import serialize from './tools/serialize-javascript'
14
15const original_findPath = Module._findPath
16
17const require_hacker =
18{
19 preceding_path_resolvers: [],
20 path_resolvers: [],
21
22 global_hook_resolved_modules: {},
23
24 occupied_file_extensions: new Set(),
25
26 // logging
27 log: new Log('require-hook', { debug: false }), // this.options.debug
28
29 // installs a global require() hook for all paths
30 //
31 // (if these paths are certain to exist in the filesystem
32 // and if you need only a specific file extension
33 // then use the .hook(extension, resolve) method instead)
34 //
35 // id - a meaningful textual identifier
36 //
37 // resolve - a function which takes one parameter:
38 //
39 // the path to be resolved
40 //
41 // must return either a javascript CommonJS module source code
42 // (i.e. "module.exports = ...", etc)
43 // or it can return nothing to fall back to the original Node.js loader
44 //
45 // returns an object with an .undo() method
46 //
47 // options:
48 //
49 // precede_node_loader:
50 //
51 // true - this require() hook will intercept all require() calls
52 // before they go into the original Node.js loader
53 //
54 // false - this require() hook will only intercept those require() calls
55 // which failed to be resolved by the original Node.js loader
56 //
57 // default value: true
58 //
59 global_hook(id, resolver, options = {})
60 {
61 validate.global_hook(id, resolver)
62
63 const resolver_entry =
64 {
65 id,
66 resolve: path =>
67 {
68 const resolved_path = `${path}.${id}`
69
70 // CommonJS module source code
71 const source = resolver(path)
72
73 if (!exists(source))
74 {
75 return
76 }
77
78 // const flush_cache = () => delete require.cache[resolved_path]
79 delete require.cache[resolved_path]
80
81 require_hacker.global_hook_resolved_modules[resolved_path] = source
82
83 return resolved_path
84 }
85 }
86
87 if (options.precede_node_loader === false)
88 {
89 require_hacker.path_resolvers.push(resolver_entry)
90 }
91 else
92 {
93 require_hacker.preceding_path_resolvers.push(resolver_entry)
94 }
95
96 const hook = this.hook(id, path =>
97 {
98 const source = require_hacker.global_hook_resolved_modules[path]
99 delete require_hacker.global_hook_resolved_modules[path]
100 return source
101 })
102
103 const result =
104 {
105 unmount: () =>
106 {
107 // javascript arrays still have no .remove() method in the XXI-st century
108 require_hacker.preceding_path_resolvers = require_hacker.preceding_path_resolvers.filter(x => x !== resolver_entry)
109 require_hacker.path_resolvers = require_hacker.path_resolvers.filter(x => x !== resolver_entry)
110 hook.unmount()
111 }
112 }
113
114 return result
115 },
116
117 // installs a require() hook for the extension
118 //
119 // extension - a file extension to hook into require()s of
120 // (examples: 'css', 'jpg', 'js')
121 //
122 // resolve - a function that takes one parameter:
123 //
124 // the path requested in the require() call
125 //
126 // must return either a javascript CommonJS module source code
127 // (i.e. "module.exports = ...", etc)
128 // or it can return nothing to fall back to the original Node.js loader
129 //
130 hook(extension, resolve)
131 {
132 this.log.debug(`Hooking into *.${extension} files loading`)
133
134 // validation
135 validate.extension(extension)
136 validate.resolve(resolve)
137
138 // occupy file extension
139 this.occupied_file_extensions.add(extension)
140
141 // dotted extension
142 const dot_extension = `.${extension}`
143
144 // keep original extension loader
145 const original_loader = Module._extensions[dot_extension]
146
147 // display a warning in case of extension loader override
148 if (original_loader)
149 {
150 // output a debug message in case of extension loader override,
151 // not a warning, so that it doesn't scare people
152 this.log.debug(`-----------------------------------------------`)
153 this.log.debug(`Overriding an already existing require() hook `)
154 this.log.debug(`for file extension ${dot_extension}`)
155 this.log.debug(`-----------------------------------------------`)
156 }
157
158 // the list of cached modules
159 const cached_modules = new Set()
160
161 // set new loader for this extension
162 Module._extensions[dot_extension] = (module, filename) =>
163 {
164 this.log.debug(`Loading source code for ${filename}`)
165
166 // var source = fs.readFileSync(filename, 'utf8')
167 const source = resolve(filename)
168
169 if (!exists(source))
170 {
171 this.log.debug(`Fallback to original loader`)
172
173 // this message would appear if there was no loader
174 // for the extension of the filename
175 if (path.extname(filename) !== dot_extension)
176 {
177 this.log.info(`Trying to load "${path.basename(filename)}" as a "*${dot_extension}"`)
178 }
179
180 // load the file with the original loader
181 return (original_loader || Module._extensions['.js'])(module, filename)
182 }
183
184 // add this file path to the list of cached modules
185 cached_modules.add(filename)
186
187 // compile javascript module from its source
188 // https://github.com/nodejs/node/blob/master/lib/module.js#L379
189 module._compile(source, filename)
190 }
191
192 const result =
193 {
194 // uninstall the hook
195 unmount: () =>
196 {
197 // clear require() cache for this file extension
198 for (let path of cached_modules)
199 {
200 delete require.cache[path]
201 }
202
203 // mount the original loader for this file extension
204 Module._extensions[dot_extension] = original_loader
205
206 // free file extension
207 this.occupied_file_extensions.delete(extension)
208 }
209 }
210
211 return result
212 },
213
214 // returns a CommonJS modules source.
215 to_javascript_module_source(anything)
216 {
217 // if the asset source wasn't found - return an empty CommonJS module
218 if (!exists(anything))
219 {
220 return 'module.exports = undefined'
221 }
222
223 // if it's already a common js module source
224 if (typeof anything === 'string' && is_a_module_declaration(anything))
225 {
226 return anything
227 }
228
229 // generate javascript module source code based on the `source` variable
230 return 'module.exports = ' + serialize(anything)
231 }
232}
233
234// validation
235const validate =
236{
237 extension(extension)
238 {
239 // if (typeof extension !== 'string')
240 // {
241 // throw new Error(`Expected string extension. Got ${extension}`)
242 // }
243
244 if (path.extname(`test.${extension}`) !== `.${extension}`)
245 {
246 throw new Error(`Invalid file extension "${extension}"`)
247 }
248
249 // check if the file extension is already occupied
250 if (require_hacker.occupied_file_extensions.has(extension))
251 {
252 throw new Error(`File extension "${extension}" is already occupied by require-hacker`)
253 }
254 },
255
256 resolve(resolve)
257 {
258 if (typeof resolve !== 'function')
259 {
260 throw new Error(`Resolve should be a function. Got "${resolve}"`)
261 }
262 },
263
264 global_hook(id, resolver)
265 {
266 if (!id)
267 {
268 throw new Error(`You must specify global hook id`)
269 }
270
271 if (path.extname(`test.${id}`) !== `.${id}`)
272 {
273 throw new Error(`Invalid global hook id "${id}". Expected a valid file extension.`)
274 }
275
276 // check if the file extension is already occupied
277 if (require_hacker.occupied_file_extensions.has(id))
278 {
279 throw new Error(`File extension "${id}" is already occupied by require-hacker`)
280 }
281
282 validate.resolve(resolver)
283 }
284}
285
286// instrument Module._findPath
287// https://github.com/nodejs/node/blob/master/lib/module.js#L335-L341
288Module._findPath = (...parameters) =>
289{
290 const request = parameters[0]
291 // const paths = parameters[1]
292
293 // preceeding resolvers
294 for (let resolver of require_hacker.preceding_path_resolvers)
295 {
296 const resolved = resolver.resolve(request)
297 if (typeof resolved !== 'undefined')
298 {
299 return resolved
300 }
301 }
302
303 // original Node.js loader
304 const filename = original_findPath.apply(undefined, parameters)
305 if (filename !== false)
306 {
307 return filename
308 }
309
310 // rest resolvers
311 for (let resolver of require_hacker.path_resolvers)
312 {
313 const resolved = resolver.resolve(request)
314 if (typeof resolved !== 'undefined')
315 {
316 return resolved
317 }
318 }
319
320 return false
321}
322
323// detect if it is a CommonJS module declaration
324function is_a_module_declaration(text)
325{
326 return text.indexOf('module.exports = ') === 0 ||
327 /\s+module\.exports = .+/.test(text)
328}
329
330export default require_hacker
\No newline at end of file